function overrideClass(clazz, overrides) {
    if (!clazz.prototype.doSuper) {
        clazz.prototype._superMethods = {};
        clazz.prototype.doSuper = function (name, args) {
            var call = this._superMethods[name];
            if (call) {
                return call.apply(this, args);
            }
        }
    }
    for (var name in overrides) {
        var superMethod = clazz.prototype[name];
        if (superMethod instanceof Function) {
            clazz.prototype._superMethods[name] = clazz.prototype[name];
        }
        clazz.prototype[name] = overrides[name];
    }
    return clazz;
}

function addLinkablePointsSupport(graph) {
    function setEdgePoint(edge, node, isFrom, linkedPoint) {
        isFrom ? edge.from = node : edge.to = node;
        setLinkedPointIndex(graph, edge, isFrom, linkedPoint.index)
    }

    function findNodeAt(evt) {
        var xy = graph.toLogical(evt);
        var x = xy.x, y = xy.y;
        var result;
        graph.forEachReverseVisibleUI(function (ui) {
            var data = ui.data;
            if (data instanceof Q.Node && ui.uiBounds.intersectsPoint(x - ui.x, y - ui.y) && ui.hitTest(x, y, 1)) {
                result = data;
                return false;
            }
        });
        return result;
    }

    function createDummyNode(x, y) {
        var dummyNode = graph.createNode();
        dummyNode.type = 'DummyNode';
        dummyNode.image = null;
        dummyNode.x = x;
        dummyNode.y = y;
        return dummyNode;
    }

    function isDummyNode(node) {
        return node.type == 'DummyNode';
    }

    graph.dataPropertyChangeDispatcher.on(function (evt) {
        if (evt.source instanceof Q.Edge && (evt.kind == 'from' || evt.kind == 'to')) {
            if (isDummyNode(evt.oldValue)) {
                graph.removeElement(evt.oldValue);
            }
        }
    })

    function drawPoint(g, p, scale) {
        var pointSize = 5;
        g.beginPath();
        var r = pointSize// / scale;
        drawCircle(g, r, p.x, p.y);
        // g.rect(p.x - r, p.y - r, r * 2, r * 2);
        g.lineWidth = 1 / scale;
        g.strokeStyle = "#F00";
        g.fillStyle = p.marked ? '#F00' : "rgba(255, 255, 255, 0.8)";
        g.stroke();
        g.fill();
    }

    function drawRect(g, rect, scale) {
        var gap = 4 / scale;
        g.beginPath();
        // if(rect.width * scale)
        g.rect(rect.x - gap, rect.y - gap, rect.width + 2 * gap, rect.height + 2 * gap);
        var x = rect.x - gap / 2, y = rect.y - gap / 2, w = rect.width + gap, h = rect.height + gap;
        g.moveTo(x, y);
        g.lineTo(x, y + h);
        g.lineTo(x + w, y + h);
        g.lineTo(x + w, y);
        g.lineTo(x, y);
        g.fillStyle = '#FFF';
        g.fill();
        g.lineWidth = 1 / scale;
        g.strokeStyle = "#555";
        g.stroke();

    }

    function getPoints(node) {
        var linkablePoints = node.linkablePoints;
        if (linkablePoints) {
            var points = [];
            var x = node.x, y = node.y;
            linkablePoints.forEach(function (p) {
                points.push({x: p.x + x, y: p.y + y})
            })
            return points;
        }
    }

    function findLinkablePoint(node, x, y) {
        if (!node) {
            graph.highlight();
            return null;
        }
        var bounds = getBodyBounds(node, graph);

        var linkedPointInfo = {node: node, bounds: bounds}

        var points = getPoints(node);
        if (!points) {
            linkedPointInfo.x = bounds.cx, linkedPointInfo.y = bounds.cy;
            graph.highlight({rects: [bounds]})
            return linkedPointInfo;
        }

        var tolerance = 20;// / graph.scale;
        var distance = tolerance, nearestPoint;
        points.forEach(function (p, index) {
            var dx = p.x - x;
            var dy = p.y - y;

            var d = Math.sqrt(dx * dx + dy * dy);
            if (d < tolerance && d < distance) {
                distance = d;
                nearestPoint = p;
                linkedPointInfo.index = index;
            }
        })
        if (nearestPoint) {
            nearestPoint.marked = true;
        }
        if (nearestPoint) {
            linkedPointInfo.x = nearestPoint.x;
            linkedPointInfo.y = nearestPoint.y;
            linkedPointInfo.offsetX = nearestPoint.x - bounds.cx;
            linkedPointInfo.offsetY = nearestPoint.y - bounds.cy;
        }
        graph.highlight({points: points, rects: [bounds]})
        return linkedPointInfo;
    }

    function getOffset(index, node) {
        var bounds = getBodyBounds(node, graph);

    }

    graph.interactionDispatcher.on(function (evt) {
        if (evt.kind == Q.InteractionEvent.POINT_MOVE_END) {
            findLinkablePoint(null);
            return;
        }
        if (evt.kind == Q.InteractionEvent.POINT_MOVING && evt.data instanceof Q.Edge && evt.point && evt.point.isEndPoint) {
            if (evt.event instanceof KeyboardEvent) {
                return;
            }
            var edge = evt.data;
            var point = evt.point;
            var isFrom = point.isFrom;
            var location = graph.toLogical(evt.event);
            var currentNode = findNodeAt(evt.event);

            if (!currentNode || !(isFrom ? graph.canLinkFrom(currentNode) : graph.canLinkTo(currentNode, edge.from))) {
                var dummyNode = createDummyNode(location.x, location.y);
                isFrom ? edge.from = dummyNode : edge.to = dummyNode;
                edge.setStyle(isFrom ? Q.Styles.EDGE_FROM_OFFSET : Q.Styles.EDGE_TO_OFFSET, null);

                findLinkablePoint(null);
                return;
            }
            if (isDummyNode(currentNode)) {
                findLinkablePoint(null);
                return;
            }
            var linkedPoint = findLinkablePoint(currentNode, location.x, location.y);
            setEdgePoint(edge, currentNode, isFrom, linkedPoint);
        }
    })

    function drawHighlight(g, highlightInfo, scale) {
        if (!highlightInfo) {
            return;
        }
        if (highlightInfo.rects) {
            highlightInfo.rects.forEach(function (p) {
                drawRect(g, p, scale)
            }.bind(this))
        }
        if (highlightInfo.points) {
            highlightInfo.points.forEach(function (p) {
                drawPoint(g, p, scale)
            }.bind(this))
        }

    }

    graph.highlight = function (options) {
        this._highlightInfo = options;
        if (options && !this._highlightLayer) {
            this._highlightLayer = function (g) {
                drawHighlight(g, this._highlightInfo, this.scale)
            }.bind(this)
            this.topCanvas.addDrawable(this._highlightLayer);
        }
        this.topCanvas.invalidate();
    }

    overrideClass(Q.CreateSimpleEdgeInteraction, {
        doDraw: function (g, scale) {
            if (!this.start || !this.currentPoint) {
                return;
            }
            var edgeUI, edgeType;
            if (this.graph.interactionProperties) {
                edgeUI = this.graph.interactionProperties.uiClass;
                edgeType = this.graph.interactionProperties.edgeType;
            }
            edgeUI = edgeUI || this.graph.edgeUIClass || Q.EdgeUI;
            edgeType = edgeType || this.graph.edgeType;

            var drawReferenceLine = edgeUI.drawReferenceLine || Q.EdgeUI.drawReferenceLine;

            var start = this.startLinkedPoint;
            if (!start) {
                start = getBodyBounds(this.start, this.graph).center
            }
            drawReferenceLine(g, start, this.currentPoint, edgeType);
            this.styleDraw(g);
        },
    })
    overrideClass(Q.CreateEdgeInteraction, {
        destroy: function (graph) {
            this.doSuper('destroy', arguments);
            if (this.graph.highlight) {
                this.graph.highlight();
            }
        },
        canLink: function (node) {
            return node instanceof Q.Node && (this.start ? this.graph.canLinkFrom(node) : this.graph.canLinkTo(node, this.start));
        },
        onmousemove: function (evt, graph) {
            var node = evt.getData();

            if (!node || !this.canLink(node)) {
                findLinkablePoint(null);
            } else {
                var location = graph.toLogical(evt);
                findLinkablePoint(node, location.x, location.y);
            }
            if (!this.start) {
                return;
            }
            this.currentPoint = this.toLogicalPoint(evt);
        },
        startdrag: function (evt, graph) {
            if (this.start || evt.responded) {
                return;
            }
            var data = evt.getData();
            if (data && this.canLink(data)) {
                evt.responded = true;
                var location = graph.toLogical(evt);
                this.start = data;
                this.startLinkedPoint = findLinkablePoint(data, location.x, location.y);
                this.addPoint(this.startLinkedPoint);
            }
        },
        onstart: function (evt, graph) {
            if (!this.start) {
                return;
            }
            var data = evt.getData();
            if (data && this.canLink(data)) {
                this.finish(evt, graph, data);
                return;
            }
            this.addPoint(this.toLogicalPoint(evt));
        },
        finish: function (evt, graph, end) {
            var location = graph.toLogical(evt);
            var linkedPoint = findLinkablePoint(end, location.x, location.y);

            var pathSegments;
            var points = this.points;
            points.pop();
            if (points.length >= 2) {
                points.shift();
                pathSegments = new Q.HashList();
                points.forEach(function (p) {
                    if (Q.isArray(p)) {
                        var p1 = p[0];
                        var p2 = p[1];
                        pathSegments.add(new Q.PathSegment(Q.Consts.SEGMENT_QUAD_TO, [p1.x, p1.y, p2.x, p2.y]));
                    } else {
                        pathSegments.add(new Q.PathSegment(Q.Consts.SEGMENT_LINE_TO, [p.x, p.y]));
                    }
                }, this);
            }

            var edge = graph.createEdgeByInteraction(this.start, end, evt, pathSegments);

            if (this.startLinkedPoint.offsetX || this.startLinkedPoint.offsetY) {
                setLinkedPointIndex(graph, edge, true, this.startLinkedPoint.index)
                this.startLinkedPoint = null;
            }
            if (linkedPoint.offsetX || linkedPoint.offsetY) {
                setLinkedPointIndex(graph, edge, false, linkedPoint.index)
            }
            this.destroy(graph);
        }
    })
}

function getBodyBounds(node, graph) {
    var ui = graph.getUI(node);
    return ui.bodyBounds;
}

///custom node
function LinkableNodeUI() {
    Q.doSuperConstructor(this, LinkableNodeUI, arguments);
}

function drawCircle(g, r, x, y) {
    var c = r * Math.tan(Math.PI / 8);
    var d = r * Math.sin(Math.PI / 4);
    g.moveTo(x + r, y);
    g.arc(x, y, r, 0, 2 * Math.PI, false);
}

LinkableNodeUI.prototype = {
    _pointRadius: 8,
    measure: function () {
        Q.doSuper(this, LinkableNodeUI, 'measure', arguments);
        var points = this.data.linkablePoints;
        if (!points) {
            return;
        }
        this.originalBounds.grow(this._pointRadius);
    },
    draw: function (g, scale) {
        Q.doSuper(this, LinkableNodeUI, 'draw', arguments);
        var points = this.data.linkablePoints;
        if (!points) {
            return;
        }
        var lineWidth = 2;
        var r = this._pointRadius - lineWidth / 2;
        g.beginPath();
        points.forEach(function (p) {
            drawCircle(g, r, p.x, p.y);
        })
      // 连点的颜色代码,完成配置后，要设置为 透明
      // g.fillStyle = '#0F0';
      g.fillStyle = 'rgba(0,0,0,0)';
      g.fill();
      g.lineWidth = lineWidth;
      // g.strokeStyle = '#FFF';
      g.strokeStyle = 'rgba(0,0,0,0)';
      g.stroke();
    }
}
Q.extend(LinkableNodeUI, Q.NodeUI);

function LinkableNode() {
    Q.doSuperConstructor(this, LinkableNode, arguments);
    this.uiClass = LinkableNodeUI;
    this.anchorPosition = Q.Position.LEFT_TOP;
    // this.resizable = false;
    this.rotatable = false;
    this.addOutProperty('linkablePoints')
}

Q.extend(LinkableNode, Q.Node);

Q.LinkableNode = LinkableNode;
if (Q.loadClassPath) {
    Q.loadClassPath(LinkableNode, 'Q.LinkableNode');
}

function setLinkedPointIndex(graph, edge, isFrom, index) {
    edge.set(isFrom ? 'fromLinkedIndex' : 'toLinkedIndex', index);
    var node = isFrom ? edge.fromAgent : edge.toAgent;
    var linkablePoints = node.linkablePoints;
    if (!linkablePoints || index === null || index === undefined || linkablePoints.length <= index) {
        edge.setStyle(isFrom ? Q.Styles.EDGE_FROM_OFFSET : Q.Styles.EDGE_TO_OFFSET, null);
        return;
    }
    var point = linkablePoints[index];
    var nodeBounds = getBodyBounds(node, graph);
    edge.setStyle(isFrom ? Q.Styles.EDGE_FROM_OFFSET : Q.Styles.EDGE_TO_OFFSET, {
        x: point.x - nodeBounds.width / 2,
        y: point.y - nodeBounds.height / 2
    });
}
